package main

import (
	"bufio"
	"bytes"
	"cmp"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strings"

	cdn20180510 "github.com/alibabacloud-go/cdn-20180510/v8/client"
	openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
	"github.com/alibabacloud-go/tea/tea"
	"github.com/aliyun/credentials-go/credentials"
)

func fatalf(format string, v ...any) {
	fmt.Fprintf(os.Stderr, format, v...)
	os.Exit(1)
}

func matchPattern(name string, patterns []string) bool {
	for _, p := range patterns {
		matched, err := filepath.Match(p, name)
		if err != nil {
			fatalf("match pattern %s failed: %v\n", p, err)
		}
		if matched {
			return true
		}
	}
	return false
}

func scanDir(rootDir string, patterns []string) ([]string, error) {
	rootDir = strings.TrimSuffix(rootDir, "/")
	var files []string
	err := filepath.WalkDir(rootDir, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			return nil
		}
		if matchPattern(d.Name(), patterns) {
			if strings.HasPrefix(path, rootDir) {
				files = append(files, path[len(rootDir)+1:])
			} else {
				rel, err := filepath.Rel(rootDir, path)
				if err != nil {
					return err
				}
				files = append(files, rel)
			}
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return files, nil
}

func readList(fname string) ([]string, error) {
	f, err := os.Open(fname)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	var list []string
	br := bufio.NewReader(f)
	for {
		ln, _, err := br.ReadLine()
		if err != nil {
			if err == io.EOF {
				break
			}
			return nil, err
		}
		ln = bytes.TrimSpace(ln)
		if len(ln) == 0 || ln[0] == '#' {
			continue
		}
		list = append(list, string(ln))
	}
	return list, nil
}

func getCdnClient(usingOssKey bool) (*cdn20180510.Client, error) {
	config := &openapi.Config{
		Endpoint: tea.String(cmp.Or(os.Getenv("ALIYUN_CDN_ENDPOINT"), "cdn.aliyuncs.com")),
	}
	if usingOssKey {
		config.AccessKeyId = tea.String(os.Getenv("OSS_ACCESS_KEY_ID"))
		config.AccessKeySecret = tea.String(os.Getenv("OSS_ACCESS_KEY_SECRET"))
	} else {
		credential, err := credentials.NewCredential(nil)
		if err != nil {
			return nil, err
		}
		config.Credential = credential
	}
	return cdn20180510.NewClient(config)
}

// refreshObjectCaches 提交刷新CDN缓存任务
func refreshObjectCaches(usingOssKey bool, objects []string) (string, error) {
	client, err := getCdnClient(usingOssKey)
	if err != nil {
		return "", err
	}
	resp, err := client.RefreshObjectCaches(&cdn20180510.RefreshObjectCachesRequest{
		ObjectPath: tea.String(strings.Join(objects, "\n")),
		ObjectType: tea.String("File"),
	})
	if err != nil {
		return "", err
	}
	return tea.StringValue(resp.Body.RefreshTaskId), nil
}

func main() {
	var (
		domain      string
		scanRoot    string
		listFile    string
		pattern     string
		maxRefresh  int
		usingOssKey bool
		dryRun      bool
		dirIndex    string
	)
	flag.StringVar(&domain, "domain", "", "domain, required")
	flag.StringVar(&scanRoot, "scan", "./dist", "scan root")
	flag.StringVar(&listFile, "list", "", "object list file")
	flag.StringVar(&pattern, "pattern", "*.html", "filter pattern, required, multiple patterns separated by ','")
	flag.IntVar(&maxRefresh, "max", 50, "max refresh object, 1-1000")
	flag.BoolVar(&dryRun, "dry-run", false, "dry run, do not refresh")
	flag.BoolVar(&usingOssKey, "oss-key", false, "refresh cdn using oss key")
	flag.StringVar(&dirIndex, "index", "", "refresh directory key if dir-index file found")
	flag.Parse()
	if domain == "" || (listFile == "" && scanRoot == "") || pattern == "" {
		flag.Usage()
		os.Exit(1)
	}
	if maxRefresh < 1 || maxRefresh > 1000 {
		fatalf("max refresh object %d must be in range 1-1000\n", maxRefresh)
	}

	// 确保 domain 以 http 或 https 开头
	scheme, _, ok := strings.Cut(domain, "://")
	if !ok {
		domain = "https://" + domain
	} else if scheme != "http" && scheme != "https" {
		fatalf("domain %s must start with http or https\n", domain)
	}

	// 确保 domain 以 / 结尾
	if !strings.HasSuffix(domain, "/") {
		domain += "/"
	}

	var objects []string
	if scanRoot != "" {
		// 确保 dirIndex 不以 / 结尾
		if strings.HasSuffix(dirIndex, "/") {
			fatalf("dir-index %s must not end with '/'\n", dirIndex)
		}
		// 确保 dirIndex 以 / 开头
		indexPage := "/" + strings.TrimPrefix(dirIndex, "/")

		patterns := strings.Split(pattern, ",")
		files, err := scanDir(scanRoot, patterns)
		if err != nil {
			fatalf("scan dir failed: %v\n", err)
		}
		objects = make([]string, 0, len(files))
		for _, file := range files {
			objectKey := domain + file
			if dryRun {
				fmt.Printf("refresh %s\n", objectKey)
			}
			objects = append(objects, objectKey)
			// 目录缓存 key
			if dirIndex != "" {
				var subIndexKey string
				if strings.HasSuffix(file, indexPage) {
					// 如果是目录索引文件，刷新目录缓存 key
					subIndexKey = domain + file[:len(file)-len(indexPage)]
				} else if file == indexPage[1:] {
					// 如果是根目录，刷新站点缓存根 key
					subIndexKey = domain
				}
				if subIndexKey != "" {
					if dryRun {
						fmt.Printf("refresh %s\n", subIndexKey)
					}
					objects = append(objects, subIndexKey)
				}
			}
		}
	} else {
		list, err := readList(listFile)
		if err != nil {
			fatalf("read list file: %v\n", err)
		}
		for _, object := range list {
			if object[0] == '/' {
				object = object[1:]
			}
			objects = append(objects, domain+object)
		}
	}

	// 跳过没有匹配的情况
	if len(objects) == 0 {
		fmt.Printf("no object found\n")
		return
	}

	if len(objects) > maxRefresh {
		fatalf("max refresh object %d, but found %d\n", maxRefresh, len(objects))
	}

	if !dryRun {
		taskId, err := refreshObjectCaches(usingOssKey, objects)
		if err != nil {
			fatalf("refresh object: %v\n", err)
		}
		fmt.Printf("refresh task submitted: %s\n", taskId)
	}
}
